﻿using System;
using System.ComponentModel;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Cache;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Xml.Serialization;

namespace RevolioReader
{
    /// <summary>
    /// Represents a Revolio book.
    /// </summary>
    [TypeConverter(typeof(ExpandableObjectConverter))]
    public class Book
    {
        /// <summary>
        /// Size of the page in one hundredth of an inch.
        /// </summary>
        private Size pageSize;

        private string XmlSource;

        /// <summary>
        /// Gets the ID of this book.
        /// </summary>
        [Description("ID of the book.")]
        public int Id { get; private set; }

        /// <summary>
        /// Gets the availability of this book.
        /// </summary>
        [Description("Indicates the availability of the book.")]
        public BookAvailability Availability { get; private set; }

        /// <summary>
        /// Gets the title of the book.
        /// </summary>
        public string Title { get; private set; }

        /// <summary>
        /// Gets the issue of the book.
        /// </summary>
        public string Issue { get; private set; }

        /// <summary>
        /// Gets the number of pages in the book.
        /// </summary>
        [Description("Number of pages in the book.")]
        public int PageCount { get; private set; }

        /// <summary>
        /// Gets the user-friendly page number of the first page in the book.
        /// </summary>
        public int PageOffset { get; private set; }

        /// <summary>
        /// Gets the size of a page in inch.
        /// For example, 5.5 x 8.5 is named "Half Letter".
        /// </summary>
        [Description("Size of a page in inch.")]
        public SizeF PageSize
        {
            get { return new SizeF(pageSize.Width / 100.0f, pageSize.Height / 100.0f); }
        }

        /// <summary>
        /// Gets the resolution of the scanned page in DPI (dots-per-inch).
        /// </summary>
        [Description("Resolution of the scanned pages in DPI (dots-per-inch).")]
        public float Resolution
        {
            get { return this.ImageWidth / this.PageSize.Width; }
        }

        /// <summary>
        /// Gets the width of the scanned images in pixels.
        /// </summary>
        public int ImageWidth { get; private set; }

        public Page[] Pages { get; private set; }

        public Section[] Sections { get; private set; }

        /// <summary>
        /// Loads a book of the given ID from the Revolio server. The
        /// Availability property of the returned object is set to indicate
        /// whether the operation is successful.
        /// </summary>
        /// <param name="id">ID of the book to load.</param>
        /// <returns>The book.</returns>
        /// <exception cref="WebException">
        /// The remote server returned an error code (such as 500).
        /// </exception>
        /// <exception cref="ArgumentOutOfRangeException">
        /// The id parameter is less than or equal to zero.
        /// </exception>
        /// <remarks>
        /// This method loads the meta data and table of contents from the
        /// server; the actual scanned pages are not loaded. 
        /// 
        /// This method shares the same cache as Internet Explorer.
        /// </remarks>
        public static Book Load(int id)
        {
            if (id <= 0)
                throw new ArgumentOutOfRangeException("id");

            Book book = new Book();
            book.Id = id;

            // Create a web client to grab contents. and set user-agent header.
            using (WebClient client = new WebClient())
            {

                // The User-Agent header must be set in order for Revolio to
                // return contents of the correct format.
                client.Headers.Add(HttpRequestHeader.UserAgent,
                    "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)");

                // Use the system-provided cache, which is shared with IE.
                client.CachePolicy = new RequestCachePolicy(RequestCacheLevel.CacheIfAvailable);

                // Set the encoding of the HTML/XML, because WebClient by 
                // default uses the system's code page.
                client.Encoding = Encoding.UTF8;

                // Retrieve and parse the front page html of the book.
                string html;
                try
                {
                    html = client.DownloadString("http://cp.revolio.com/issue/" + id);
                }
                catch (WebException ex)
                {
                    HttpWebResponse response = ex.Response as HttpWebResponse;
                    if (response != null && response.StatusCode == HttpStatusCode.NotFound)
                    {
                        book.Availability = BookAvailability.NotFound;
                        return book;
                    }
                    throw ex;
                }
                book.ParseHtml(html);
                if (book.Availability != BookAvailability.Available)
                    return book;

                // Retrieve and parse the book definition XML.
                string xml = client.DownloadString(book.XmlSource);
                book.ParseXml(xml);
                return book;
            }
        }

        /// <summary>
        /// Extracts book meta data from an HTML page.
        /// </summary>
        /// <exception cref="NotSupportedException">If the format of the
        /// HTML cannot be parsed.</exception>
        /// <param name="html">HTML source from which meta data about the
        /// book is extracted.</param>
        public void ParseHtml(string html)
        {
            // Search for text patterns in the html to fill the book info.
            Match m;

            // var g_id = 1234;
            m = new Regex(@"var\s+g_id\s*=\s*(\d+)",
                RegexOptions.Multiline).Match(html);
            if (m.Success)
                this.Id = Int32.Parse(m.Groups[1].Value);

            // var g_totalPages = 123;
            if (m.Success)
                m = new Regex(@"var\s+g_totalPages\s*=\s*(\d+)",
                    RegexOptions.Multiline).Match(html);
            if (m.Success)
                this.PageCount = Int32.Parse(m.Groups[1].Value);

            // var g_titleName = "title";
            if (m.Success)
                m = new Regex(@"var\s+g_titleName\s*=\s*""(.*?)""",
                    RegexOptions.Multiline).Match(html);
            if (m.Success)
                this.Title = HttpUtility.HtmlDecode(m.Groups[1].Value);

            // var g_issueName = "sub-title";
            if (m.Success)
                m = new Regex(@"var\s+g_issueName\s*=\s*""(.*?)""",
                    RegexOptions.Multiline).Match(html);
            if (m.Success)
                this.Issue = HttpUtility.HtmlDecode(m.Groups[1].Value);

            // xmlSource=...&
            if (m.Success)
                m = new Regex(@"xmlSource=(.*?)&",
                    RegexOptions.Multiline).Match(html);
            if (m.Success)
                this.XmlSource = m.Groups[1].Value;

            // If the above REQUIRED fields cannot be extracted, try find
            // other proof that the book is indeed not available, i.e. not
            // because of a change in HTML format.
            if (!m.Success)
            {
                // <title>This Issue was deleted by the author.</title>
                m = new Regex(@"<title>.*?deleted.*?</title>",
                    RegexOptions.Multiline).Match(html);
                if (m.Success)
                {
                    this.Availability = BookAvailability.Deleted;
                    return;
                }

                // <title>This Issue has been disabled by the author.</title>
                m = new Regex(@"<title>.*?disabled.*?</title>",
                    RegexOptions.Multiline).Match(html);
                if (m.Success)
                {
                    this.Availability = BookAvailability.Disabled;
                    return;
                }

                // <title>Sorry, this demo Issue has expired.</title>
                m = new Regex(@"<title>.*?expired.*?</title>",
                    RegexOptions.Multiline).Match(html);
                if (m.Success)
                {
                    this.Availability = BookAvailability.Expired;
                    return;
                }

                // var g_showLogin = 0;
                m = new Regex(@"var\s+g_showLogin\s*=\s*1\s*;",
                    RegexOptions.Multiline).Match(html);
                if (m.Success) // login required
                {
                    this.Availability = BookAvailability.Forbidden;
                    return;
                }

                // If we can't find positive evidence that the book is
                // unavailable, we treat the html as unsupported.
                throw new NotSupportedException("The HTML code could not be understood.");
            }

            // Now we can extract optional information.
            // [optional] var g_pageNumOffset = -12;
            m = new Regex(@"var\s+g_pageNumOffset\s*=\s*([+-]*\d+)",
                RegexOptions.Multiline).Match(html);
            this.PageOffset = m.Success ? Int32.Parse(m.Groups[1].Value) : 0;

            // Mark the book as Available and finish.
            this.Availability = BookAvailability.Available;
        }

        public void ParseXml(string xml)
        {
            //value.Replace('\n', ' ');
            if (xml.IndexOf("<FlippingBook>") >= 0) // old-style definition
            {
                XmlSerializer serializer = new XmlSerializer(
                    typeof(Revolio.Schema.V1.FlippingBook));
                Revolio.Schema.V1.FlippingBook fb = (Revolio.Schema.V1.FlippingBook)
                    serializer.Deserialize(new StringReader(xml));

                this.pageSize.Width = fb.width / 2;
                this.pageSize.Height = fb.height;
                this.ImageWidth = fb.zoomWidth;
                this.PageOffset = fb.firstPage;
                this.Pages = (from imageUrl in fb.Pages
                              select new Page(imageUrl)
                              ).ToArray<Page>();
                this.Sections = (from a in fb.Articles
                                 select new Section
                                 {
                                     Title = a.title,
                                     PageNumber = a.Page
                                 }).ToArray<Section>();
            }
            else // new-style definition
            {
                XmlSerializer serializer = new XmlSerializer(
                    typeof(Revolio.Schema.V2.BookContents));
                Revolio.Schema.V2.BookContents b = (Revolio.Schema.V2.BookContents)
                    serializer.Deserialize(new StringReader(xml));

                this.pageSize.Width = b.configuration.width / 2;
                this.pageSize.Height = b.configuration.height;
                this.ImageWidth = b.configuration.zoomWidth;
                this.Pages = (from p in b.Pages
                              select new Page(p.thumb)
                              ).ToArray<Page>();
                this.Sections = (from a in b.Articles
                                 select new Section
                                 {
                                     Title = a.title.Replace('\n', ' '),
                                     PageNumber = a.page
                                 }).ToArray<Section>();
            }
        }
    }

    /// <summary>
    /// Represents a page in a book or magazine.
    /// </summary>
    public class Page
    {
        private string thumbnailUrl;

        /// <summary>
        /// Creates a page with the given thumbnail URL.
        /// </summary>
        /// <param name="thumbnailUrl">URL of the thumbnail image.</param>
        public Page(string thumbnailUrl)
        {
            this.thumbnailUrl = thumbnailUrl;
        }

        /// <summary>
        /// Gets the URL of the low-resolution scanned image.
        /// </summary>
        public string ThumbnailUrl
        {
            get { return thumbnailUrl; }
        }

        /// <summary>
        /// Gets the URL of the high-resolution scanned image.
        /// </summary>
        public string ImageUrl
        {
            get
            {
                string url = this.thumbnailUrl;
                return url.Substring(0, url.LastIndexOf('/') + 1) + "0.jpg";
            }
        }

        /// <summary>
        /// Loads the image data from the server.
        /// </summary>
        public byte[] GetData()
        {
            using (WebClient client = new WebClient())
            {
                client.CachePolicy = new RequestCachePolicy(RequestCacheLevel.CacheIfAvailable);
                client.Headers.Add(HttpRequestHeader.UserAgent,
                    "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)");
                return client.DownloadData(this.ImageUrl);
            }
        }

        /// <summary>
        /// Loads the image from the server. The caller must call Dispose()
        /// on the returned object when it is no longer used.
        /// </summary>
        public Image GetImage()
        {
            return Image.FromStream(new MemoryStream(GetData()));
        }
    }

    /// <summary>
    /// Represents a section in a book or an article in a magazine.
    /// </summary>
    public class Section
    {
        /// <summary>
        /// Gets or sets the title of the section.
        /// </summary>
        public string Title { get; set; }

        /// <summary>
        /// Gets or sets the page number of the first page in the section.
        /// </summary>
        public int PageNumber { get; set; }
    }

    /// <summary>
    /// Indicates the availability of a book or magazine.
    /// </summary>
    public enum BookAvailability
    {
        /// <summary>
        /// Indicates that the availability of the book has either not been 
        /// checked or been checked but could not be determined.
        /// </summary>
        Unknown,

        /// <summary>
        /// Indicates that the book is available for reading and downloading.
        /// </summary>
        Available,

        /// <summary>
        /// Indicates that a login is required to access the book.
        /// </summary>
        Forbidden,

        /// <summary>
        /// Indicates that a book with the given id cannot be found on the server.
        /// </summary>
        NotFound,

        /// <summary>
        /// Indicates that the book was deleted by the author.
        /// </summary>
        Deleted,

        /// <summary>
        /// Indicates that the book has been disabled by the author.
        /// </summary>
        Disabled,

        /// <summary>
        /// Indicates that the demo book has expired.
        /// </summary>
        Expired,
    }
}

namespace Revolio.Schema.V1
{
    /// <summary>
    /// Represents the root element in the old-style book definition XML.
    /// </summary>
    public class FlippingBook
    {
        public int width;
        public int height;
        public bool alwaysOpened;
        public int firstPage;
        public int zoomWidth;

        [XmlArrayItem("Page")]
        public string[] Pages;

        [XmlArrayItem("Article")]
        public Article[] Articles;
    }

    public class Article
    {
        public int ID;
        public string title;
        public int Page;
    }
}

namespace Revolio.Schema.V2
{
    /// <summary>
    /// Represents the root element in the new-style book definition XML.
    /// </summary>
    [XmlRoot("xml")]
    public class BookContents
    {
        public Configuration configuration;

        [XmlArray("pages")]
        [XmlArrayItem("page")]
        public Page[] Pages;

        [XmlArray("articles")]
        [XmlArrayItem("item")]
        public Article[] Articles;
    }

    public class Configuration
    {
        public int id;
        public int width;
        public int height;
        public int zoomWidth;
        public bool alwaysOpened;
    }

    public class Page
    {
        public int height;
        public int width;
        public string swf;
        public string thumb;
    }

    public class Article
    {
        public int id;
        public string title;
        public int page;
    }
}
